Skip to content

fix(providers): init options.hooks before merging node hooks#1235

Closed
mayckol wants to merge 1 commit intocoleam00:devfrom
mayckol:fix/claude-provider-hooks-undefined-merge
Closed

fix(providers): init options.hooks before merging node hooks#1235
mayckol wants to merge 1 commit intocoleam00:devfrom
mayckol:fix/claude-provider-hooks-undefined-merge

Conversation

@mayckol
Copy link
Copy Markdown

@mayckol mayckol commented Apr 15, 2026

fix(providers): init options.hooks before merging node hooks

Summary

applyNodeConfig is called twice in sendQuery:

  1. Once against a throwaway {} as Options to extract provider warnings (line 898).
  2. Once against the real Options built by buildBaseClaudeOptions, which pre-seeds hooks with the PostToolUse tool-capture hook (line 941).

The first call crashed whenever a node declared hooks: in its nodeConfig, because the merge loop wrote directly into options.hooks without checking it existed. This blocks every DAG workflow that uses per-node hooks (e.g. the analyze node of archon-architect) at the very first AI layer.

Error reproduced

TypeError: undefined is not an object (evaluating 'options.hooks[event] = matchers')
    at applyNodeConfig (packages/providers/src/claude/provider.ts:393:20)
    at sendQuery (packages/providers/src/claude/provider.ts:899:34)
    at processTicksAndRejections (native:7:39)

Before:
image

After:
image

Reproduction: run any DAG workflow whose first AI node defines hooks: in its YAML. The stock archon-architect workflow (analyze node with PreToolUse/PostToolUse hooks) hits it deterministically — scan-metrics completes in ~100 ms, then analyze fails instantly with the TypeError and the remaining 5 nodes are skipped.

Fix

Initialize options.hooks = {} before the merge loop so the subsequent options.hooks[event] = ... assignments are safe regardless of whether the caller pre-populated the hooks map. The second call path (line 941) is unaffected because its options.hooks is already populated by buildBaseClaudeOptions.

Test plan

  • New regression test in provider.test.ts exercises sendQuery with nodeConfig.hooks and asserts both the node hook and the provider's PostToolUse capture hook land on the SDK options. Fails on dev without the patch, passes with it.
  • bun run type-check — all packages pass
  • bun run lint — clean
  • bun run format:check — clean
  • bun run test — all packages pass (64 tests in @archon/providers alone)
  • Manually retried archon-architect end-to-end after the patch: analyze node streams normally for minutes instead of crashing at ~1 ms.

applyNodeConfig is called twice in sendQuery: once against a throwaway
`{} as Options` to extract warnings, then again against the real Options
produced by buildBaseClaudeOptions (which seeds the PostToolUse capture
hook). The first call crashed with

  TypeError: undefined is not an object (evaluating
    'options.hooks[event] = matchers')

whenever a node declared `hooks:` in its nodeConfig, because the merge
loop assigned directly into `options.hooks` without ensuring it existed.
This blocked every DAG workflow that relied on per-node hooks (e.g.
archon-architect's analyze node) at the first AI layer.

Initialize `options.hooks = {}` before the merge loop so the assignment
is safe regardless of whether the caller pre-populated the hooks map.

Also add a regression test that exercises sendQuery with
`nodeConfig.hooks` and asserts both the node hook and the provider's
PostToolUse capture hook land on the SDK options.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 15, 2026

📝 Walkthrough

Walkthrough

This change adds a regression test for hook propagation in ClaudeProvider.sendQuery and implements defensive initialization of options.hooks in applyNodeConfig to prevent runtime crashes when hook merging occurs on undefined hooks fields.

Changes

Cohort / File(s) Summary
Hook Propagation Test
packages/providers/src/claude/provider.test.ts
New regression test that verifies nodeConfig.hooks (e.g., PreToolUse) are correctly propagated to the underlying SDK query call and that the provider's PostToolUse hook is present.
Defensive Hook Initialization
packages/providers/src/claude/provider.ts
Added defensive initialization of options.hooks to an empty SDKHooksMap in applyNodeConfig when hook merging is needed but the hooks field is missing/undefined, preventing runtime crashes during merge operations.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~8 minutes

Possibly related PRs

Poem

🐰 A hook here, a hook there,
But when hooks go missing in the air,
Our fuzzy defender stands tall and true,
With defensive init, the crash breaks through! ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main fix: initializing options.hooks before merging node hooks to prevent a runtime crash.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Description check ✅ Passed The PR description is comprehensive, well-structured, and addresses all major template sections including problem statement, fix details, error reproduction, test plan, and validation evidence.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@Wirasm
Copy link
Copy Markdown
Collaborator

Wirasm commented Apr 22, 2026

Thanks for diagnosing and fixing this, @mayckol — closing because the fix landed independently via #1177 yesterday. Both PRs root-cause identically to the warning-extraction pathway calling `applyNodeConfig` against a throwaway `{} as Options`, and both fix it the same way (`if (!options.hooks) options.hooks = {}` before the merge loop).

Dev currently has:

```ts
// Merge with existing hooks (PostToolUse capture hook)
const existingHooks = options.hooks as SDKHooksMap | undefined;
if (!options.hooks) {
(options as Record<string, unknown>).hooks = {};
}
```

which is functionally the same as your patch — so the code change is a no-op on current `dev`.

However, your regression test is the piece #1177 didn't include and would be valuable to land on its own. The test asserts both sides of the invariant (`nodeConfig.hooks` lands on SDK options AND the provider's own PostToolUse capture hook is preserved), which is exactly the kind of lock-in that would have caught this class of bug earlier and would guard against any future refactor in `applyNodeConfig` dropping one or the other.

If you'd like, open a small test-only follow-up PR with just the `'passes hooks to SDK via nodeConfig without crashing on warning extraction'` test from this branch. The code in `applyNodeConfig` is already what your test expects, so the test lands cleanly and locks the invariant down. Happy to merge that fast.

Thanks again for catching this independently — glad the `archon-architect` workflow now works for you.

@Wirasm Wirasm closed this Apr 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants